#include "telnetchecker.h"

#include <iostream>
#include <netdb.h>
#include <sys/socket.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>

// 默认设置最大3秒的连接超时时间
#define MAX_SOCK_CONNECT_TIMEOUT (3)

int connectTimeout(int sockfd, struct sockaddr *addr, socklen_t len, int timeoutValue);

TelnetChecker::TelnetChecker(const std::string &host, const int port)
    : m_host(host)
    , m_port(port)
{
}

TelnetChecker::~TelnetChecker()
{
}

bool TelnetChecker::check()
{
    std::cout << "Checking telnet " << m_host << ":" << m_port << std::endl;
    if (0 == m_host.size() or m_port <= 0) {
        std::cout << "head request param invalid" << std::endl;
        return false;
    }

    std::cout << "start network detection..." << std::endl;
    struct addrinfo hints;
    struct addrinfo *result, *rp;

    memset(&hints, 0, sizeof(hints));
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;

    if (::getaddrinfo(m_host.c_str(), std::to_string(m_port).c_str(), &hints, &result) != 0) {
        std::cout << "resolving domain failed, domain:" << m_host << ", port:" << m_port << std::endl;
        return false;
    }

    int sockfd {};
    for (rp = result; rp != nullptr; rp = rp->ai_next) {
        sockfd = ::socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
        if (sockfd == -1) {
            continue;
        }

        if (connectTimeout(sockfd, rp->ai_addr, rp->ai_addrlen, MAX_SOCK_CONNECT_TIMEOUT) != -1) {
            ::close(sockfd);
            ::freeaddrinfo(result);
            std::cout << "connectivity has connected" << std::endl;
            return true;
        }

        ::close(sockfd);
    }

    ::freeaddrinfo(result);
    std::cout << "network closed or unreachable, domain:" << m_host << ", port:" << m_port << std::endl;
    return false;
}

void setSockBlockMode(int fd, bool block)
{
    /*fcntl函数功能为获取或设置文件描述符属性*/
    int flg = fcntl(fd, F_GETFL);
    if (flg == -1) {
        return;
    }

    if (block) {
        flg &= ~O_NONBLOCK;
    } else {
        flg |= O_NONBLOCK;
    }

    fcntl(fd, F_SETFL, flg);
}

int connectTimeout(int sockfd, struct sockaddr *addr, socklen_t len, int timeoutValue)
{
    int ret = 0;
    /*将socket描述符设置为非阻塞模式,准备做连接超时处理流程*/
    setSockBlockMode(sockfd, false);
    ret = connect(sockfd, addr, len);
    /* #define	EINPROGRESS	115	 Operation now in progress  */
    if (ret != -1 or errno != EINPROGRESS) {
        return ret;
    }

    fd_set conn_fdset;
    FD_ZERO(&conn_fdset);
    FD_SET(sockfd, &conn_fdset);

    struct timeval timeout;
    timeout.tv_sec = timeoutValue;
    timeout.tv_usec = 0;
    do {
        /*将连接conn_fdset集合表设置到select事件管理中的写入集合,等待写入事件响应*/
        ret = select(sockfd + 1, nullptr, &conn_fdset, nullptr, &timeout);
    } while (ret == -1 and errno == EINTR); /*若在设置的超时时间内,响应事件错误,且错误原因为被信号中断,则继续循环*/

    if (ret == 0) { /*select超时*/
        /* #define	ETIMEDOUT	110	 Connection timed out  */
        /*将错误码传递给主调函数,让其判断,并执行连接超时的相应处理*/
        errno = ETIMEDOUT;
        ret = -1;
    } else if (ret == -1) { /*select出错*/
        ret = -1;
    } else if (ret > 0) { /*select响应写入事件*/
        /* 当select机制监控到conn_fdset可读，并不能代表连接是可用的。还需要做进一步的异常判断处理
         * 造成可读有两种结果：1 连接建立成功, 2 连接建立失败,失败会回写失败信息,造成可读。
         * 此时错误信息不会保存至errno变量中，因此，需要调用getsockopt来获取。
         */
        int conn_err;
        socklen_t sock_len = sizeof(conn_err);
        if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &conn_err, &sock_len) == -1) { /*getsockopt出错*/
            ret = -1;
        } else { /*getsockopt成功获取socket属性*/
            if (conn_err == 0) { /*当conn_err为0,表示建立连接成功*/
                ret = 0; /*设置为0返回,表示成功*/
            } else { /*当conn_err不为0,表示建立连接失败*/
                errno = conn_err;
                ret = -1;
            }
        }
    }

    return ret;
}
